// 
//  Copyright © 2008, 2009 Jiří Zárevúcky <zarevucky.jiri@gmail.com>
// 
//  This program is free software: you can redistribute it and/or modify
//  it under the terms of the GNU Affero General Public License as
//  published by the Free Software Foundation, either version 3 of the
//  License, or (at your option) any later version.
// 
//  This program is distributed in the hope that it will be useful,
//  but WITHOUT ANY WARRANTY; without even the implied warranty of
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//  GNU Affero General Public License for more details.
// 
//  You should have received a copy of the GNU Affero General Public License
//  along with this program.  If not, see <http://www.gnu.org/licenses/>.
// 
// 

using System;
using System.Linq;
using System.Collections;
using System.Collections.ObjectModel;
using System.Collections.Generic;

using Anculus.Core;

using Galaxium.Protocol.Xmpp.Library.Core;
using Galaxium.Protocol.Xmpp.Library.Utility;
using Galaxium.Protocol.Xmpp.Library.Messaging;
using Galaxium.Protocol.Xmpp.Library.Helpers;

namespace Galaxium.Protocol.Xmpp.Library
{
	public class ContactEventArgs: EventArgs
	{
		public Contact Contact { get; private set; }
		public ContactEventArgs (Contact contact) { Contact = contact; }
	}
	
	public class Contact: IEnumerable<ResourceInfo>
	{
		private List<string> _resource_order =
			new List<string> ();
		
		private Dictionary<string, ResourceInfo> _resources =
			new Dictionary<string, ResourceInfo> ();
		
		#region properties

		public string              Name         { get; private set; }
		public bool                IsAsking     { get; private set; }
		public SubscriptionState   Subscription { get; private set; }
		public ICollection<string> Groups       { get; private set; }
		
		public JabberID  Jid        { get;  private set; }	
		public Client    Client     { get;  private set; }
		public bool      IsWaiting  { get; internal set; }
		public bool      HasError   { get;  private set; }
		
		public bool      IsInRoster { get;  private set; }
		
		public bool IsSubscribed {
			get {
				return (Subscription == SubscriptionState.To ||
				        Subscription == SubscriptionState.Both);
			}
		}

		public bool HasSubscription {
			get {
				return (Subscription == SubscriptionState.From ||
				        Subscription == SubscriptionState.Both);
			}
		}
		
		public bool IsOnline {
			get { return (_resources.Count > 0 && !HasError); }
		}

		public IEnumerable<ResourceInfo> Resources {
			get {
				var list = new List<ResourceInfo> (_resources.Values);
				list.Sort ();
				return list;
			}
		}
		
		public int ResourceCount {
			get { return _resources.Count; }
		}
		
		public Presence MainPresence {
			get {
				if (_resources.Count == 0) return new Presence (PresType.Unavailable, null);
				return _resources [_resource_order [0]].Presence;
			}
		}

		#endregion

		internal Contact (Client client, JabberID jid)
		{
			if (jid.IsFull)
				throw new ArgumentException ("Jid must be bare.");
			Client = client;
			Jid = jid;
			Subscription = SubscriptionState.None;
			Groups = new ReadOnlyCollection<string> (new string[] {});
		}
		
		internal Contact (Client cl, RosterItem source)
			:this (cl, source.Jid)
		{
			Update (source);
		}
		
		internal void Update (RosterItem source)
		{
			Name = source.Name;
			IsAsking = source.Asking;
			Subscription = source.Subscription;
			
			bool is_being_added = (Subscription != SubscriptionState.Remove && !IsInRoster);
			if (is_being_added)
				IsInRoster = true;
			
			bool is_being_removed = (Subscription == SubscriptionState.Remove);
			if (is_being_removed) {
				Subscription = SubscriptionState.None;
				if (!IsInRoster) {
					Log.Warn ("Received a remove push before an adding one");
					return; // prevents crash if server sends a remove item without previous push
				}
				IsInRoster = false;
			}
			
			IsWaiting = IsWaiting && !HasSubscription;
			
			////////////////////////////////////////////////////////////////////////////////

			var old_groups = new List<string> (Groups);
			var new_groups = new List<string> (source.Groups);

			if (!is_being_added && old_groups.Count == 0)
				old_groups.Add (String.Empty);
			
			if (!is_being_removed && new_groups.Count == 0)
				new_groups.Add (String.Empty);
			
			foreach (string group in new_groups.Except (old_groups))
				Client.Roster.AddToGroup (group, this);
			
			foreach (string group in old_groups.Except (new_groups))
				Client.Roster.RemoveFromGroup (group, this);
			
			Groups = new ReadOnlyCollection<string> (source.Groups.ToArray ());
		}

		internal void ImportPresence (Presence pres)
		{
			if (pres.Type == PresType.Unavailable ||
			    (pres.Type == PresType.Error && pres.From.IsFull)) {

				if (_resources.ContainsKey (pres.From.Resource ?? String.Empty)) {
					_resource_order.Remove (pres.From.Resource ?? String.Empty);
					_resources.Remove (pres.From.Resource ?? String.Empty);
				}
			} else {
				if (pres.Type == PresType.Error && pres.From.IsBare) {
					ClearPresences ();
					HasError = true;
				} else if (HasError) {
					ClearPresences ();
					HasError = false;
				}					
				
				ResourceInfo resource;
				if (!_resources.TryGetValue (pres.From.Resource ?? String.Empty, out resource)) {
					resource = new ResourceInfo (this, pres.From.Resource);
					_resources [pres.From.Resource ?? String.Empty] = resource;
					_resource_order.Add (pres.From.Resource ?? String.Empty);
				}
				
				resource.UpdatePresence (pres);
			}

			// keep resource list sorted by presence's priority
			_resource_order.Sort ((a, b) => - _resources [a].CompareTo (_resources [b]));
		}

		internal void ClearPresences ()
		{
			_resources.Clear ();
			_resource_order.Clear ();
		}

		IEnumerator IEnumerable.GetEnumerator ()
		{
			return GetEnumerator ();
		}
		
		public IEnumerator<ResourceInfo> GetEnumerator ()
		{
			foreach (string resource in _resource_order)
				yield return _resources [resource];
		}

		public ResourceInfo GetResource (string resource)
		{
			ResourceInfo info;
			if (_resources.TryGetValue (resource, out info)) return info;
			return null;
		}

		public RosterItem GetItem ()
		{
			RosterItem item = new RosterItem (Jid);
			item.Name = Name;
			item.SetGroups (Groups);
			return item;
		}
	}
}